// $Id: session.c 41 2011-04-15 15:44:51Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2010 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#ifdef WIN32
#include <winsock2.h>
#define ETIMEDOUT -1
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#endif

#include <nio.h>
#include <nio/buffer.h>
#include <nio/future.h>

#include "log.h"
#include "response.h"

#include "session.h"

struct niohttp_session *niohttp_session_new(
		struct nioreactor *reactor, struct niobuffer *buffer,
		struct niosocket *socket)
{
	struct niohttp_session *session;

	niohttp_debug("constructing");

	if (!(session = calloc(1, sizeof(struct niohttp_session)))) {
		niohttp_error("failed to calloc");
		goto error;
	}

	if (!(session->future = niofuture_new(reactor))) {
		niohttp_error("failed to create event");
		goto error;
	}

	if (!(session->response = niohttp_response_new())) {
		niohttp_error("failed to create response");
		goto error;
	}

	session->buffer = buffer;
	session->socket = socket;
	return session;

error:
	niohttp_session_free(session);
	return NULL;
}

void niohttp_session_free(struct niohttp_session *session)
{
	if (!session) {
		return;
	}

	niohttp_debug("destructing");

	if (session->response) {
		niohttp_response_free(session->response);
	}

	if (session->future) {
		niofuture_free(session->future);
	}

	free(session);
}

int niohttp_session_submit(struct niohttp_session *session,
		struct niohttp_request *req, struct niobuffer *content,
		niohttp_request_cb cb, void *cbarg)
{
	int rc;
	char buffer[32];

	if (!session || !req || !cb) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	if (req->session) {
		niohttp_error("request in session: %p", req->session);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	if (session->request) {
		niohttp_error("request pending: %p", session->request);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	if (session->tunnel_cb) {
		niohttp_error("tunnel pending: %p", session->tunnel_cb);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	niohttp_debug("starting: %s %s", req->method, req->uri);

	niobuffer_clear(session->buffer);

	if (content) {
		snprintf(buffer, sizeof(buffer), "%d",
				niobuffer_remaining(content));

		if ((rc = niohttp_request_set_header(req, "Content-Length",
				buffer))) {
			niohttp_error("failed to set content length header");
			goto finally;
		}
	}

	if ((rc = niohttp_request_format(req, session->buffer))) {
		niohttp_error("failed to format request");
		goto finally;
	}

	if (content) {
		if ((rc = niobuffer_load(session->buffer, content))) {
			niohttp_error("failed to load buffer: %s",
					nio_strerror(rc));
			rc = NIOHTTP_EBUFFER;
			goto finally;
		}
	}

	niobuffer_flip(session->buffer);

	if ((rc = niofuture_writable(session->future, session->socket,
			niohttp_session_timeout(session),
			niohttp_session_socket_cb, session))) {
		niohttp_error("failed to schedule for write: %s",
				nio_strerror(rc));
		rc = NIOHTTP_EIO;
		goto finally;
	}

	req->session = session;
	req->cb = cb;
	req->cbarg = cbarg;
	session->request = req;
	niohttp_response_reset(session->response);
	rc = 0;

finally:
	return rc;
}

int niohttp_session_pending(struct niohttp_session *session)
{
	return session->request || session->tunnel_cb;
}

int niohttp_session_cancel(struct niohttp_session *session)
{
	int rc;

	if (!session) {
		niohttp_error("null argument");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	if (!session->request && !session->tunnel_cb) {
		niohttp_error("not pending");
		rc = NIOHTTP_ENOENT;
		goto finally;
	}

	niohttp_debug("canceling");

	if ((rc = niofuture_cancel(session->future))) {
		niohttp_error("failed to cancel event: %s", nio_strerror(rc));
		goto finally;
	}

	session->request->session = NULL;
	session->request = NULL;
	session->tunnel_cb = NULL;
	session->tunnel_cbarg = NULL;
	rc = 0;

finally:
	return rc;
}

struct timeval *niohttp_session_timeout(struct niohttp_session *session)
{
	if (session->timeout == 0) {
		return NULL;
	} else {
		session->tv.tv_sec = session->timeout;
		session->tv.tv_usec = 0;
		return &session->tv;
	}
}

void niohttp_session_socket_cb(struct niofuture *ev,
		struct niosocket *socket, int flags, void *cbarg)
{
	int rc;
	struct niohttp_session *session = cbarg;

	niohttp_debug("processing socket event: %d", flags);

	if (flags & NIO_TIMEUP) {
		niohttp_error("timeout: %d second", session->timeout);
		rc = ETIMEDOUT;
	} else if (flags & NIO_WRITABLE) {
		if ((rc = niohttp_session_on_writable(session))) {
			niohttp_error("failed to send request");
			goto finally;
		}
	} else if (flags & NIO_READABLE) {
		if ((rc = niohttp_session_on_readable(session))) {
			niohttp_error("failed to receive response");
			goto finally;
		}
	} else {
		niohttp_error("unknown flags: %d", flags);
		rc = -1;
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		session->response->code = -1;
		session->response->reason = NULL;

		if (niohttp_session_dispatch(session)) {
			niohttp_error("failed to dispatch failed request");
		}
	}
}

int niohttp_session_on_writable(struct niohttp_session *session)
{
	int rc;

	niohttp_debug("writing request: %s %s", session->request->method,
			session->request->uri);

	if ((rc = niobuffer_write(session->buffer, session->socket))) {
		niohttp_error("failed to write request");
		goto finally;
	}

	if (niobuffer_remaining(session->buffer) > 0) {
		niohttp_debug("scheduling to write remaining request");

		if ((rc = niofuture_writable(session->future, session->socket,
				niohttp_session_timeout(session),
				niohttp_session_socket_cb, session))) {
			niohttp_error("failed to schedule for write");
			goto finally;
		}
	} else {
		niobuffer_clear(session->buffer);

		niohttp_debug("scheduling to read response");

		if ((rc = niofuture_readable(session->future, session->socket,
				niohttp_session_timeout(session),
				niohttp_session_socket_cb, session))) {
			niohttp_error("failed to schedule for read");
			goto finally;
		}
	}

	rc = 0;

finally:
	return rc;
}

int niohttp_session_on_readable(struct niohttp_session *session)
{
	int rc;
	struct niohttp_response *resp = session->response;
	size_t len;

	niohttp_debug("reading response for: %s %s", session->request->method,
			session->request->uri);

	len = niobuffer_position(session->buffer);

	if ((rc = niobuffer_read(session->buffer, session->socket))) {
		niohttp_error("failed to read response: %s",
				nio_strerror(rc));
		goto finally;
	}

	len -= niobuffer_position(session->buffer);

	if (len == 0) {
		niohttp_error("no data to read");
		rc = -1;
		goto finally;
	}

	niohttp_debug("flipping buffer for parsing");

	niobuffer_flip(session->buffer);

	niohttp_debug("parsing response from buffer");

	if ((rc = niohttp_response_parse(resp, session->buffer))) {
		niohttp_error("failed to parse response");
		goto finally;
	}

	if (resp->state != NIOHTTP_RESP_METADATA) {
		niohttp_debug("scheduling for remaining header");

		if ((rc = niofuture_readable(session->future, session->socket,
				niohttp_session_timeout(session),
				niohttp_session_socket_cb, session))) {
			niohttp_error("failed to schedule for read");
			goto finally;
		}
	} else {
		niohttp_debug("processing response");

		if (strcmp(session->request->protocol, resp->protocol)) {
			niohttp_error("protocol mismatch: %s != %s",
					session->request->protocol,
					resp->protocol);
			goto finally;
		}

		if (strcmp(session->request->version.string,
				resp->version.string)) {
			niohttp_error("version mismatch: %s != %s",
					session->request->version.string,
					resp->version.string);
			goto finally;
		}

		if ((rc = niohttp_session_dispatch(session))) {
			niohttp_error("failed to dispatch request");
			goto finally;
		}
	}

	rc = 0;

finally:
	return rc;
}

int niohttp_session_dispatch(struct niohttp_session *session)
{
	int rc;
	struct niohttp_request *req;

	niohttp_debug("dispatching response");

	req = session->request;
	session->request->session = NULL;
	session->request = NULL;

	niohttp_debug("invoking: %p", req->cb);

	(*req->cb)(req, session->response, req->cbarg);
	rc = 0;

	return rc;
}

int niohttp_session_writable(struct niohttp_session *session,
		niohttp_session_tunnel_cb cb, void *cbarg)
{
	int rc;

	if (!session || !cb) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	if (session->request) {
		niohttp_error("request pending: %p", session->request);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	if (session->tunnel_cb) {
		niohttp_error("tunnel pending: %p", session->tunnel_cb);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	niohttp_debug("opening tunnel for write: %p", cb);

	if ((rc = niofuture_writable(session->future, session->socket,
			niohttp_session_timeout(session),
			niohttp_session_tunnel_writable_cb, session))) {
		niohttp_error("failed to schedule for tunneling write: %s",
				nio_strerror(rc));
		rc = NIOHTTP_EIO;
		goto finally;
	}

	session->tunnel_cb = cb;
	session->tunnel_cbarg = cbarg;
	rc = 0;

finally:
	return rc;
}

void niohttp_session_tunnel_writable_cb(struct niofuture *ev,
		struct niosocket *socket, int flags, void *cbarg)
{
	struct niohttp_session *session = cbarg;
	niohttp_session_tunnel_cb cb = session->tunnel_cb;

	session->tunnel_cb = NULL;

	niohttp_debug("invoking for tunnel writable: %p", cb);

	(*cb)(session,socket, session->tunnel_cbarg);
}

int niohttp_session_readable(struct niohttp_session *session,
		niohttp_session_tunnel_cb cb, void *cbarg)
{
	int rc;

	if (!session || !cb) {
		niohttp_error("null argument(s)");
		rc = NIOHTTP_EINVAL;
		goto finally;
	}

	if (session->request) {
		niohttp_error("request pending: %p", session->request);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	if (session->tunnel_cb) {
		niohttp_error("tunnel pending: %p", session->tunnel_cb);
		rc = NIOHTTP_EEXIST;
		goto finally;
	}

	niohttp_debug("opening tunnel for read: %p", cb);

	if ((rc = niofuture_readable(session->future, session->socket,
			niohttp_session_timeout(session),
			niohttp_session_tunnel_readable_cb, session))) {
		niohttp_error("failed to schedule for tunneling write: %s",
				nio_strerror(rc));
		rc = NIOHTTP_EIO;
		goto finally;
	}

	session->tunnel_cb = cb;
	session->tunnel_cbarg = cbarg;
	rc = 0;

finally:
	return rc;
}

void niohttp_session_tunnel_readable_cb(struct niofuture *ev,
		struct niosocket *socket, int flags, void *cbarg)
{
	struct niohttp_session *session = cbarg;
	niohttp_session_tunnel_cb cb = session->tunnel_cb;

	session->tunnel_cb = NULL;

	niohttp_debug("invoking for tunnel readable: %p", cb);

	(*cb)(session, socket, session->tunnel_cbarg);
}
